/*
 * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.lang.invoke;

import java.util.ArrayList;
import java.util.Arrays;

import static java.lang.invoke.LambdaForm.*;
import static java.lang.invoke.LambdaForm.BasicType.*;

/**
 * Working storage for an LF that is being transformed.
 * Similarly to a StringBuffer, the editing can take place in multiple steps.
 */
final class LambdaFormBuffer {

  private int arity, length;
  private Name[] names;
  private Name[] originalNames;  // snapshot of pre-transaction names
  private byte flags;
  private int firstChange;
  private Name resultName;
  private String debugName;
  private ArrayList<Name> dups;

  private static final int F_TRANS = 0x10, F_OWNED = 0x03;

  LambdaFormBuffer(LambdaForm lf) {
    this.arity = lf.arity;
    setNames(lf.names);
    int result = lf.result;
    if (result == LAST_RESULT) {
      result = length - 1;
    }
    if (result >= 0 && lf.names[result].type != V_TYPE) {
      resultName = lf.names[result];
    }
    debugName = lf.debugName;
    assert (lf.nameRefsAreLegal());
  }

  private LambdaForm lambdaForm() {
    assert (!inTrans());  // need endEdit call to tidy things up
    return new LambdaForm(debugName, arity, nameArray(), resultIndex());
  }

  Name name(int i) {
    assert (i < length);
    return names[i];
  }

  Name[] nameArray() {
    return Arrays.copyOf(names, length);
  }

  int resultIndex() {
    if (resultName == null) {
      return VOID_RESULT;
    }
    int index = indexOf(resultName, names);
    assert (index >= 0);
    return index;
  }

  void setNames(Name[] names2) {
    names = originalNames = names2;  // keep a record of where everything was to start with
    length = names2.length;
    flags = 0;
  }

  private boolean verifyArity() {
    for (int i = 0; i < arity && i < firstChange; i++) {
      assert (names[i].isParam()) : "#" + i + "=" + names[i];
    }
    for (int i = arity; i < length; i++) {
      assert (!names[i].isParam()) : "#" + i + "=" + names[i];
    }
    for (int i = length; i < names.length; i++) {
      assert (names[i] == null) : "#" + i + "=" + names[i];
    }
    // check resultName also
    if (resultName != null) {
      int resultIndex = indexOf(resultName, names);
      assert (resultIndex >= 0) : "not found: " + resultName.exprString() + Arrays.asList(names);
      assert (names[resultIndex] == resultName);
    }
    return true;
  }

  private boolean verifyFirstChange() {
    assert (inTrans());
    for (int i = 0; i < length; i++) {
      if (names[i] != originalNames[i]) {
        assert (firstChange == i) : Arrays
            .asList(firstChange, i, originalNames[i].exprString(), Arrays.asList(names));
        return true;
      }
    }
    assert (firstChange == length) : Arrays.asList(firstChange, Arrays.asList(names));
    return true;
  }

  private static int indexOf(NamedFunction fn, NamedFunction[] fns) {
    for (int i = 0; i < fns.length; i++) {
      if (fns[i] == fn) {
        return i;
      }
    }
    return -1;
  }

  private static int indexOf(Name n, Name[] ns) {
    for (int i = 0; i < ns.length; i++) {
      if (ns[i] == n) {
        return i;
      }
    }
    return -1;
  }

  boolean inTrans() {
    return (flags & F_TRANS) != 0;
  }

  int ownedCount() {
    return flags & F_OWNED;
  }

  void growNames(int insertPos, int growLength) {
    int oldLength = length;
    int newLength = oldLength + growLength;
    int oc = ownedCount();
    if (oc == 0 || newLength > names.length) {
      names = Arrays.copyOf(names, (names.length + growLength) * 5 / 4);
      if (oc == 0) {
        flags++;
        oc++;
        assert (ownedCount() == oc);
      }
    }
    if (originalNames != null && originalNames.length < names.length) {
      originalNames = Arrays.copyOf(originalNames, names.length);
      if (oc == 1) {
        flags++;
        oc++;
        assert (ownedCount() == oc);
      }
    }
    if (growLength == 0) {
      return;
    }
    int insertEnd = insertPos + growLength;
    int tailLength = oldLength - insertPos;
    System.arraycopy(names, insertPos, names, insertEnd, tailLength);
    Arrays.fill(names, insertPos, insertEnd, null);
    if (originalNames != null) {
      System.arraycopy(originalNames, insertPos, originalNames, insertEnd, tailLength);
      Arrays.fill(originalNames, insertPos, insertEnd, null);
    }
    length = newLength;
    if (firstChange >= insertPos) {
      firstChange += growLength;
    }
  }

  int lastIndexOf(Name n) {
    int result = -1;
    for (int i = 0; i < length; i++) {
      if (names[i] == n) {
        result = i;
      }
    }
    return result;
  }

  /**
   * We have just overwritten the name at pos1 with the name at pos2.
   * This means that there are two copies of the name, which we will have to fix later.
   */
  private void noteDuplicate(int pos1, int pos2) {
    Name n = names[pos1];
    assert (n == names[pos2]);
    assert (originalNames[pos1] != null);  // something was replaced at pos1
    assert (originalNames[pos2] == null || originalNames[pos2] == n);
    if (dups == null) {
      dups = new ArrayList<>();
    }
    dups.add(n);
  }

  /**
   * Replace duplicate names by nulls, and remove all nulls.
   */
  private void clearDuplicatesAndNulls() {
    if (dups != null) {
      // Remove duplicates.
      assert (ownedCount() >= 1);
      for (Name dup : dups) {
        for (int i = firstChange; i < length; i++) {
          if (names[i] == dup && originalNames[i] != dup) {
            names[i] = null;
            assert (Arrays.asList(names).contains(dup));
            break;  // kill only one dup
          }
        }
      }
      dups.clear();
    }
    // Now that we are done with originalNames, remove "killed" names.
    int oldLength = length;
    for (int i = firstChange; i < length; i++) {
      if (names[i] == null) {
        System.arraycopy(names, i + 1, names, i, (--length - i));
        --i;  // restart loop at this position
      }
    }
    if (length < oldLength) {
      Arrays.fill(names, length, oldLength, null);
    }
    assert (!Arrays.asList(names).subList(0, length).contains(null));
  }

  /**
   * Create a private, writable copy of names.
   * Preserve the original copy, for reference.
   */
  void startEdit() {
    assert (verifyArity());
    int oc = ownedCount();
    assert (!inTrans());  // no nested transactions
    flags |= F_TRANS;
    Name[] oldNames = names;
    Name[] ownBuffer = (oc == 2 ? originalNames : null);
    assert (ownBuffer != oldNames);
    if (ownBuffer != null && ownBuffer.length >= length) {
      names = copyNamesInto(ownBuffer);
    } else {
      // make a new buffer to hold the names
      final int SLOP = 2;
      names = Arrays.copyOf(oldNames, Math.max(length + SLOP, oldNames.length));
      if (oc < 2) {
        ++flags;
      }
      assert (ownedCount() == oc + 1);
    }
    originalNames = oldNames;
    assert (originalNames != names);
    firstChange = length;
    assert (inTrans());
  }

  private void changeName(int i, Name name) {
    assert (inTrans());
    assert (i < length);
    Name oldName = names[i];
    assert (oldName == originalNames[i]);  // no multiple changes
    assert (verifyFirstChange());
    if (ownedCount() == 0) {
      growNames(0, 0);
    }
    names[i] = name;
    if (firstChange > i) {
      firstChange = i;
    }
    if (resultName != null && resultName == oldName) {
      resultName = name;
    }
  }

  /**
   * Change the result name.  Null means a void result.
   */
  void setResult(Name name) {
    assert (name == null || lastIndexOf(name) >= 0);
    resultName = name;
  }

  /**
   * Finish a transaction.
   */
  LambdaForm endEdit() {
    assert (verifyFirstChange());
    // Assuming names have been changed pairwise from originalNames[i] to names[i],
    // update arguments to ensure referential integrity.
    for (int i = Math.max(firstChange, arity); i < length; i++) {
      Name name = names[i];
      if (name == null) {
        continue;  // space for removed duplicate
      }
      Name newName = name.replaceNames(originalNames, names, firstChange, i);
      if (newName != name) {
        names[i] = newName;
        if (resultName == name) {
          resultName = newName;
        }
      }
    }
    assert (inTrans());
    flags &= ~F_TRANS;
    clearDuplicatesAndNulls();
    originalNames = null;
    // If any parameters have been changed, then reorder them as needed.
    // This is a "sheep-and-goats" stable sort, pushing all non-parameters
    // to the right of all parameters.
    if (firstChange < arity) {
      Name[] exprs = new Name[arity - firstChange];
      int argp = firstChange, exprp = 0;
      for (int i = firstChange; i < arity; i++) {
        Name name = names[i];
        if (name.isParam()) {
          names[argp++] = name;
        } else {
          exprs[exprp++] = name;
        }
      }
      assert (exprp == (arity - argp));
      // copy the exprs just after the last remaining param
      System.arraycopy(exprs, 0, names, argp, exprp);
      // adjust arity
      arity -= exprp;
    }
    assert (verifyArity());
    return lambdaForm();
  }

  private Name[] copyNamesInto(Name[] buffer) {
    System.arraycopy(names, 0, buffer, 0, length);
    Arrays.fill(buffer, length, buffer.length, null);
    return buffer;
  }

  /**
   * Replace any Name whose function is in oldFns with a copy
   * whose function is in the corresponding position in newFns.
   * Only do this if the arguments are exactly equal to the given.
   */
  LambdaFormBuffer replaceFunctions(NamedFunction[] oldFns, NamedFunction[] newFns,
      Object... forArguments) {
    assert (inTrans());
    if (oldFns.length == 0) {
      return this;
    }
    for (int i = arity; i < length; i++) {
      Name n = names[i];
      int nfi = indexOf(n.function, oldFns);
      if (nfi >= 0 && Arrays.equals(n.arguments, forArguments)) {
        changeName(i, new Name(newFns[nfi], n.arguments));
      }
    }
    return this;
  }

  private void replaceName(int pos, Name binding) {
    assert (inTrans());
    assert (verifyArity());
    assert (pos < arity);
    Name param = names[pos];
    assert (param.isParam());
    assert (param.type == binding.type);
    changeName(pos, binding);
  }

  /**
   * Replace a parameter by a fresh parameter.
   */
  LambdaFormBuffer renameParameter(int pos, Name newParam) {
    assert (newParam.isParam());
    replaceName(pos, newParam);
    return this;
  }

  /**
   * Replace a parameter by a fresh expression.
   */
  LambdaFormBuffer replaceParameterByNewExpression(int pos, Name binding) {
    assert (!binding.isParam());
    assert (lastIndexOf(binding) < 0);  // else use replaceParameterByCopy
    replaceName(pos, binding);
    return this;
  }

  /**
   * Replace a parameter by another parameter or expression already in the form.
   */
  LambdaFormBuffer replaceParameterByCopy(int pos, int valuePos) {
    assert (pos != valuePos);
    replaceName(pos, names[valuePos]);
    noteDuplicate(pos, valuePos);  // temporarily, will occur twice in the names array
    return this;
  }

  private void insertName(int pos, Name expr, boolean isParameter) {
    assert (inTrans());
    assert (verifyArity());
    assert (isParameter ? pos <= arity : pos >= arity);
    growNames(pos, 1);
    if (isParameter) {
      arity += 1;
    }
    changeName(pos, expr);
  }

  /**
   * Insert a fresh expression.
   */
  LambdaFormBuffer insertExpression(int pos, Name expr) {
    assert (!expr.isParam());
    insertName(pos, expr, false);
    return this;
  }

  /**
   * Insert a fresh parameter.
   */
  LambdaFormBuffer insertParameter(int pos, Name param) {
    assert (param.isParam());
    insertName(pos, param, true);
    return this;
  }
}
